/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 */
package org.opends.server.util.cli;

import static com.forgerock.opendj.cli.Utils.*;

import static org.opends.messages.ToolMessages.*;

import java.io.PrintStream;
import java.util.LinkedList;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.ssl.SSLException;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
import org.opends.server.tools.LDAPConnection;
import org.opends.server.tools.LDAPConnectionException;
import org.opends.server.tools.LDAPConnectionOptions;
import org.opends.server.tools.SSLConnectionException;
import org.opends.server.tools.SSLConnectionFactory;
import org.opends.server.types.OpenDsException;

import com.forgerock.opendj.cli.Argument;
import com.forgerock.opendj.cli.ArgumentException;
import com.forgerock.opendj.cli.ArgumentGroup;
import com.forgerock.opendj.cli.ArgumentParser;
import com.forgerock.opendj.cli.ClientException;
import com.forgerock.opendj.cli.ConsoleApplication;
import com.forgerock.opendj.cli.FileBasedArgument;
import com.forgerock.opendj.cli.StringArgument;

/**
 * Creates an argument parser pre-populated with arguments for specifying
 * information for opening and LDAPConnection an LDAP connection.
 */
public class LDAPConnectionArgumentParser extends ArgumentParser
{
  private SecureConnectionCliArgs args;

  /**
   * Creates a new instance of this argument parser with no arguments. Unnamed
   * trailing arguments will not be allowed.
   *
   * @param mainClassName
   *          The fully-qualified name of the Java class that should be invoked
   *          to launch the program with which this argument parser is
   *          associated.
   * @param toolDescription
   *          A human-readable description for the tool, which will be included
   *          when displaying usage information.
   * @param longArgumentsCaseSensitive
   *          Indicates whether long arguments should
   * @param argumentGroup
   *          Group to which LDAP arguments will be added to the parser. May be
   *          null to indicate that arguments should be added to the default
   *          group
   * @param alwaysSSL
   *          If true, always use the SSL connection type. In this case, the
   *          arguments useSSL and startTLS are not present.
   */
  public LDAPConnectionArgumentParser(String mainClassName, LocalizableMessage toolDescription,
      boolean longArgumentsCaseSensitive, ArgumentGroup argumentGroup, boolean alwaysSSL)
  {
    super(mainClassName, toolDescription, longArgumentsCaseSensitive);
    addLdapConnectionArguments(argumentGroup, alwaysSSL);
    setVersionHandler(new DirectoryServerVersionHandler());
  }

  /**
   * Creates a new LDAPConnection and invokes a connect operation using
   * information provided in the parsed set of arguments that were provided by
   * the user.
   *
   * @param out
   *          stream to write messages
   * @param err
   *          stream to write error messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException
   *           if there was a problem connecting to the server indicated by the
   *           input arguments
   * @throws ArgumentException
   *           if there was a problem processing the input arguments
   */
  public LDAPConnection connect(PrintStream out, PrintStream err) throws LDAPConnectionException, ArgumentException
  {
    return connect(this.args, out, err);
  }

  /**
   * Creates a new LDAPConnection and invokes a connect operation using
   * information provided in the parsed set of arguments that were provided by
   * the user.
   *
   * @param args
   *          with which to connect
   * @param out
   *          stream to write messages
   * @param err
   *          stream to write error messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException
   *           if there was a problem connecting to the server indicated by the
   *           input arguments
   * @throws ArgumentException
   *           if there was a problem processing the input arguments
   */
  private LDAPConnection connect(SecureConnectionCliArgs args, PrintStream out, PrintStream err)
      throws LDAPConnectionException, ArgumentException
  {
    throwIfArgumentsConflict(args.getBindPasswordArg(), args.getBindPasswordFileArg());
    throwIfArgumentsConflict(args.getKeyStorePasswordArg(), args.getKeyStorePasswordFileArg());
    throwIfArgumentsConflict(args.getTrustStorePasswordArg(), args.getTrustStorePasswordFileArg());
    throwIfArgumentsConflict(args.getUseSSLArg(), args.getUseStartTLSArg());

    // Create the LDAP connection options object, which will be used to
    // customize the way that we connect to the server and specify a set of
    // basic defaults.
    LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
    connectionOptions.setVersionNumber(3);

    // See if we should use SSL or StartTLS when establishing the connection.
    // If so, then make sure only one of them was specified.
    if (args.getUseSSLArg().isPresent())
    {
      connectionOptions.setUseSSL(true);
    }
    else if (args.getUseStartTLSArg().isPresent())
    {
      connectionOptions.setStartTLS(true);
    }

    // If we should blindly trust any certificate, then install the appropriate
    // SSL connection factory.
    if (args.getUseSSLArg().isPresent() || args.getUseStartTLSArg().isPresent())
    {
      try
      {
        String clientAlias;
        if (args.getCertNicknameArg().isPresent())
        {
          clientAlias = args.getCertNicknameArg().getValue();
        }
        else
        {
          clientAlias = null;
        }

        SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory();
        sslConnectionFactory.init(args.getTrustAllArg().isPresent(),
                                  args.getKeyStorePathArg().getValue(),
                                  args.getKeyStorePasswordArg().getValue(),
                                  clientAlias,
                                  args.getTrustStorePathArg().getValue(),
                                  args.getTrustStorePasswordArg().getValue());
        connectionOptions.setSSLConnectionFactory(sslConnectionFactory);
      }
      catch (SSLConnectionException sce)
      {
        printWrappedText(err, ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL.get(sce.getMessage()));
      }
    }

    // If one or more SASL options were provided, then make sure that one of
    // them was "mech" and specified a valid SASL mechanism.
    if (args.getSaslOptionArg().isPresent())
    {
      String mechanism = null;
      LinkedList<String> options = new LinkedList<>();

      for (String s : args.getSaslOptionArg().getValues())
      {
        int equalPos = s.indexOf('=');
        if (equalPos <= 0)
        {
          printAndThrowException(err, ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION.get(s));
        }
        else
        {
          String name = s.substring(0, equalPos);
          if ("mech".equalsIgnoreCase(name))
          {
            mechanism = s;
          }
          else
          {
            options.add(s);
          }
        }
      }

      if (mechanism == null)
      {
        printAndThrowException(err, ERR_LDAP_CONN_NO_SASL_MECHANISM.get());
      }

      connectionOptions.setSASLMechanism(mechanism);
      for (String option : options)
      {
        connectionOptions.addSASLProperty(option);
      }
    }

    int timeout = args.getConnectTimeoutArg().getIntValue();

    final String passwordValue = getPasswordValue(
            args.getBindPasswordArg(), args.getBindPasswordFileArg(), args.getBindDnArg(), out, err);
    return connect(
            args.getHostNameArg().getValue(),
            args.getPortArg().getIntValue(),
            args.getBindDnArg().getValue(),
            passwordValue,
            connectionOptions, timeout, out, err);
  }

  private void printAndThrowException(PrintStream err, LocalizableMessage message) throws ArgumentException
  {
    printWrappedText(err, message);
    throw new ArgumentException(message);
  }

  /**
   * Creates a connection using a console interaction that will be used to
   * potentially interact with the user to prompt for necessary information for
   * establishing the connection.
   *
   * @param ui
   *          user interaction for prompting the user
   * @param out
   *          stream to write messages
   * @param err
   *          stream to write error messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws SSLConnectionException
   *           if there was a problem connecting with SSL to the server
   * @throws LDAPConnectionException
   *           if there was any other problem connecting to the server
   * @throws ArgumentException
   *           if there was a problem indicated by the input arguments
   */
  public LDAPConnection connect(LDAPConnectionConsoleInteraction ui, PrintStream out, PrintStream err)
      throws LDAPConnectionException, SSLConnectionException, ArgumentException
  {
    try
    {
      ui.run();
      LDAPConnectionOptions options = new LDAPConnectionOptions();
      options.setVersionNumber(3);
      return connect(ui.getHostName(), ui.getPortNumber(), ui.getBindDN().toString(),
          ui.getBindPassword(), ui.populateLDAPOptions(options), ui.getConnectTimeout(), out, err);
    }
    catch (OpenDsException e)
    {
      err.println(isSSLException(e) ?
          ERR_TASKINFO_LDAP_EXCEPTION_SSL.get(ui.getHostName(), ui.getPortNumber()) : e.getMessageObject());
      throw e;
    }
  }

  private boolean isSSLException(Exception e)
  {
    return e.getCause() != null
        && e.getCause().getCause() != null
        && e.getCause().getCause() instanceof SSLException;
  }

  /**
   * Creates a connection from information provided.
   *
   * @param host
   *          of the server
   * @param port
   *          of the server
   * @param bindDN
   *          with which to connect
   * @param bindPw
   *          with which to connect
   * @param options
   *          with which to connect
   * @param timeout
   *          the timeout to establish the connection in milliseconds. Use
   *          {@code 0} to express no timeout
   * @param out
   *          stream to write messages
   * @param err
   *          stream to write error messages
   * @return LDAPConnection created by this class from parsed arguments
   * @throws LDAPConnectionException
   *           if there was a problem connecting to the server indicated by the
   *           input arguments
   */
  private LDAPConnection connect(String host, int port, String bindDN, String bindPw, LDAPConnectionOptions options,
      int timeout, PrintStream out, PrintStream err) throws LDAPConnectionException
  {
    // Attempt to connect and authenticate to the Directory Server.
    AtomicInteger nextMessageID = new AtomicInteger(1);
    LDAPConnection connection = new LDAPConnection(host, port, options, out, err);
    connection.connectToHost(bindDN, bindPw, nextMessageID, timeout);
    return connection;
  }

  /**
   * Gets the arguments associated with this parser.
   *
   * @return arguments for this parser.
   */
  public SecureConnectionCliArgs getArguments()
  {
    return args;
  }

  /**
   * Commodity method that retrieves the password value analyzing the contents
   * of a string argument and of a file based argument. It assumes that the
   * arguments have already been parsed and validated. If the string is a dash,
   * or no password is available, it will prompt for it on the command line.
   *
   * @param bindPwdArg
   *          the string argument for the password.
   * @param bindPwdFileArg
   *          the file based argument for the password.
   * @param bindDnArg
   *          the string argument for the bindDN.
   * @param out
   *          stream to write message.
   * @param err
   *          stream to write error message.
   * @return the password value.
   */
  public static String getPasswordValue(StringArgument bindPwdArg, FileBasedArgument bindPwdFileArg,
      StringArgument bindDnArg, PrintStream out, PrintStream err)
  {
    try
    {
      return getPasswordValue(bindPwdArg, bindPwdFileArg, bindDnArg.getValue(), out, err);
    }
    catch (Exception ex)
    {
      printWrappedText(err, ex.getMessage());
      return null;
    }
  }

  /**
   * Commodity method that retrieves the password value analyzing the contents
   * of a string argument and of a file based argument. It assumes that the
   * arguments have already been parsed and validated. If the string is a dash,
   * or no password is available, it will prompt for it on the command line.
   *
   * @param bindPassword
   *          the string argument for the password.
   * @param bindPasswordFile
   *          the file based argument for the password.
   * @param bindDNValue
   *          the string value for the bindDN.
   * @param out
   *          stream to write message.
   * @param err
   *          stream to write error message.
   * @return the password value.
   * @throws ClientException
   *           if the password cannot be read
   */
  public static String getPasswordValue(StringArgument bindPassword, FileBasedArgument bindPasswordFile,
      String bindDNValue, PrintStream out, PrintStream err) throws ClientException
  {
    String bindPasswordValue = bindPassword.getValue();
    if ("-".equals(bindPasswordValue)
        || (!bindPasswordFile.isPresent() && bindDNValue != null && bindPasswordValue == null))
    {
      // read the password from the stdin.
      out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue));
      char[] pwChars = ConsoleApplication.readPassword();
      // As per rfc 4513(section-5.1.2) a client should avoid sending
      // an empty password to the server.
      while (pwChars.length == 0)
      {
        printWrappedText(err, INFO_LDAPAUTH_NON_EMPTY_PASSWORD.get());
        out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue));
        pwChars = ConsoleApplication.readPassword();
      }
      return new String(pwChars);
    }
    else if (bindPasswordValue == null)
    {
      // Read from file if it exists.
      return bindPasswordFile.getValue();
    }
    return bindPasswordValue;
  }

  private void addLdapConnectionArguments(ArgumentGroup argGroup, boolean alwaysSSL)
  {
    args = new SecureConnectionCliArgs(alwaysSSL);
    try
    {
      Set<Argument> argSet = args.createGlobalArguments();
      for (Argument arg : argSet)
      {
        addArgument(arg, argGroup);
      }
    }
    catch (ArgumentException ae)
    {
      ae.printStackTrace(); // Should never happen
    }
  }
}
